Repository navigation
Add json_document and json_view: node index, parser, and document - #5620
Merged
Merged
Conversation
nlohmann
added this pull request to stack #5636
September 29, 2026 14:20
Owner
Author
|
CI fixes for this PR (commits 87a041a and ab4f646):
Verified with the CI's own image — posted by Claude Code on behalf of @nlohmann |
This was referenced Sep 29, 2026
nlohmann
removed this pull request from stack #5636
September 30, 2026 13:19
nlohmann
force-pushed
the
json-view/08-view-builder
branch
from
September 30, 2026 13:20
ab4f646 to
dc9899f
Compare
nlohmann
added this pull request to stack #5739
September 30, 2026 13:21
nlohmann
force-pushed
the
json-view/08-view-builder
branch
3 times, most recently
from
September 30, 2026 18:06
7030188 to
c195988
Compare
nlohmann
marked this pull request as ready for review
September 30, 2026 18:18
nlohmann
force-pushed
the
json-view/08-view-builder
branch
from
September 30, 2026 18:19
c195988 to
e96e298
Compare
nlohmann
removed this pull request from stack #5739
October 6, 2026 09:26
nlohmann
force-pushed
the
json-view/08-view-builder
branch
from
October 6, 2026 09:28
2739c0b to
4303ba6
Compare
nlohmann
changed the base branch from
json-view/04-unicode-escapes
to
json-view/23-zmij
October 6, 2026 09:29
nlohmann
added this pull request to stack #5768
October 6, 2026 09:29
nlohmann
force-pushed
the
json-view/08-view-builder
branch
from
October 7, 2026 14:44
4303ba6 to
759dd2e
Compare
🔴 Amalgamation check failed! 🔴The source code has not been amalgamated and/or formatted correctly, or 📎 A ready-to-apply patch is attached to the failed workflow run as the git apply amalgamation.patchThis does not require installing astyle yourself. |
json_document::parse(std::move(const_string)) failed to compile with "no matching read_kind": only a non-const rvalue std::string can be moved from. Treat a const rvalue as a copied byte container. parse() does not accept everything BasicJsonType::parse() does: a FILE* and pointers to or arrays of wide characters are rejected at compile time. List the supported inputs instead. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
explicit operator bool meant "refers to a value", which silently differs from what a basic_json converts to. Use !v.is_discarded() instead. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
auto v = json_document::parse(text).root() compiled and left the view dangling. Delete the overload for rvalue documents, take a named document in the tests, and document the lifetime rule. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The non-little-endian path of the node writer wrote the integer's native word over len and next, so len got the high half there. Compose and split the value explicitly (len is the low half, next the high half); the little-endian path stays a plain memcpy. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
reserve(n) and the growth of the index computed n * sizeof(node) without a check, which wraps around on 32-bit targets for inputs of about 1 GiB and allocates a too small array. Throw std::bad_alloc for a count beyond the address space and clamp the growth step to it. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The check rejects inputs of 0xFFFFFFF0 bytes or more, but the exception message and the documentation said 4 GiB. Name the limit once (max_input_size), and state 4 GiB minus 16 bytes in the message and the documentation. Test the limit with a container that only claims the size. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Nothing reads it: the view does not compare values. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The fuzzer only parsed a std::string with default options. Also parse an exact-size byte vector, which has no NUL after its last byte and takes the bounds-checked path, and derive ignore_comments and ignore_trailing_commas from the first input byte, comparing against json::parse and json::accept with the same options. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
ci_single_binaries runs IWYU with --error on every header. For json_view.hpp it suggested adding <array> (std::array is used), the headers that json.hpp already provides (abi_config, abi_macros, input_adapters, json_pointer, cpp_future, string_concat, value_t, json_fwd), and <version> for std::nullptr_t, and removing <cstddef>. - include <array> - keep <cstddef> (nullptr_t, size_t; IWYU attributes them to <version> and <cstring>) - tell IWYU not to suggest the headers that json.hpp provides: the amalgamated json_view.hpp only includes json.hpp, so including them here would duplicate their definitions - export json.hpp, keep macro_unscope.hpp, and drop the unused forward declaration of the document class Signed-off-by: Niels Lohmann <mail@nlohmann.me>
emit() moves the node array when it grows, and the subtraction read base in the same expression, so the order was unspecified. MSVC Release builds without forced inlining read the old base; the container index then pointed outside the array and close() wrote out of bounds. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
const_text is const, so std::move does not move from it; the second call is part of the const rvalue test. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Changes promised in the replies to the review of #5620: - read() reuses the owned copy: a copied input (an rvalue byte container, a stream, FILE*, wide characters, an iterator range) is appended to the cleared owned buffer instead of a fresh std::string being move-assigned over it. A moved-in std::string still replaces it. - The error replay runs the library parser with a SAX handler that stores nothing and throws the reported exception, instead of BasicJsonType::parse(), which built a DOM up to the error. - Pointers to and arrays of wide characters, and FILE*, take the input adapter path (converted to UTF-8 into an owned buffer) instead of failing a static_assert. Wide pointers are NUL-terminated. - Streams are read in blocks through rdbuf() into the buffer, instead of character by character through the input adapter; eofbit is set at the end, as by the library's stream adapter. - tape/tape_size/tape_cap are now nodes/node_count/node_capacity. - The builder's members get descriptive, m_-prefixed names. - The shallow frame stack is a std::array (in a wrapper with a user-provided constructor: -Weffc++ wants the member initialized, and value-initializing the 64 frames costs up to 30 % on small documents). - The value dispatch macro has no NEXT parameter; the switch is left with break, and the macro is undefined after its last use. - literal() and digits_exceed() deduce the length from the string literal; each literal has one helper; the two `if (!is_float)` blocks are merged. - arena_ensure() sits right before the writes; the decode arena is a std::vector<char>. - The builder records the deepest nesting, and materialize() sizes its stack from it once (growing it only if a subtree is deeper). - Comment fixes (hex4, slow_string, the NUL rule, put_utf8, 0 - m, the arena resize, grow()) and formatting (macro backslashes, digit cases, one power of ten per line, value_t assertion next to is_container()). - build_with's template parameters follow the builder's order. Greg's idea of keeping the source offset in `off` and the arena offset in `next` for escaped strings was measured and not adopted: string access got 7-18 % slower with a select, and 3.5-18 % slower with a branch-free field pick (Apple M-series, clang -O3), parsing was unchanged. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
- Copying is disabled because of its cost, not because of two documents borrowing the same buffer; the unique_ptr follows from move-only. - Inputs: streams, FILE*, and wide characters (strings, arrays, pointers) are all supported and owned; point to parse_copy() for owned copies. - The error replay only runs for failing input, builds no values, and is skipped with allow_exceptions = false. - read() writes copied inputs into the previous owned copy. - Views can dangle like std::string_view, and why there is no shared/weak pointer design. - The 4 GiB limit (16-byte nodes, 16-byte scanner steps), streams being read to the end first, which macros apply, why materialize() has no source positions, and wording nits (accept() vs basic_json::accept(), is_binary, is_discarded, see-also lists, size table, "back into"). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…son_view - inline_nodes is null when the document has no inline nodes, instead of pointing past the end of the allocation; shrink_to_fit() no longer passes it to memcpy() for an empty node array - is_byte_char and is_wide_char use conjunction/disjunction, which astyle leaves unspaced - append_stream() uses (std::max) for the count read Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The test calls detail::view::materialize(), which only the single header pulled in; the multiple-header build (JSON_TEST_USING_MULTIPLE_HEADERS) failed with "'materialize' is not a member of detail::view". Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Qt defines emit as an empty macro, so a Qt header included before json_view.hpp broke the declaration of the builder's emit(), see issue #5865. It is now called push_node(). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
memcpy() must not be passed a null pointer even for zero bytes, which arena_run() did for an empty run while the arena was still empty (ci_test_clang_sanitizer: "null pointer passed as argument 1" in builder.hpp). Only the slow path can see such a run. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Infer's std::unique_ptr model confuses the values that the test's build() helper returns by value and reports USE_AFTER_DELETE for one of them after the destructor of an unrelated temporary (ci_infer, 12 reports, all in tests/src/unit-json_view_builder.cpp). AddressSanitizer is clean on the same tests. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
MSVC 2015 does not treat selecting a deleted function in decltype as a substitution failure, so is_detected reports the deleted parse/parse_copy/accept/read(ptr, len) overloads as callable and the static_asserts in "integer arguments do not compile" fail (AppVeyor VS2015 jobs). Calling them still fails to compile there. Guard the negative checks as unit-delete_deprecated_functions.cpp does. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This was referenced Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the stack for the zero-copy view (#5295). This adds the internal node index and parser, and the public
json_document/json_viewclasses built on it. Element access, values,dump(), and comparisons follow in later PRs, so each one stays reviewable.Summary
max_nodes()when the index grows; beyond it,std::bad_allocis thrown.json::parseaccepts: every combination ofignore_commentsandignore_trailing_commas, with and without a trailing NUL, and underJSON_STRICT_NUL_HANDLING. It is portable C++11, and nothing depends on the byte order: words are read in a fixed order, node fields are written explicitly, and literals are compared withmemcmp. The SIMD kernels come later in the stack.basic_json_document<BasicJsonType>:parse,parse_copy,accept,read(reuses the document's memory),root,is_discarded,source,owns_source,node_count,memory_usage,shrink_to_fit.basic_json_view<BasicJsonType>:type, theis_*queries,size,empty,materialize,source_offset.json_document,json_view,ordered_json_document,ordered_json_view.std::string_view, C strings, character arrays, pointer ranges, and in C++20 other contiguous iterator ranges) or owned (a non-const rvaluestd::string, moved in; a const rvaluestd::stringand other rvalue byte containers, copied; streams and wide strings, read into a buffer). AFILE*and pointers to or arrays of wide characters are rejected at compile time.parse,parse_copy,accept, andreadwith an integer as the second argument are deleted:parse(ptr, len)would silently convertlentoallow_exceptionsand readptrwithstrlen. Useparse(ptr, ptr + len).basic_json::parsewould throw for the same input. The library parser runs on the failing input (a cold path), so message, position, and id match.out_of_range.416("input of 4294967280 bytes or more is not supported by json_document").materialize(): builds the value with the SAX handlerparse()uses, soJSON_DIAGNOSTICSparent pointers are set as usual.root()of a temporary document is deleted: the view would dangle. Give the document a name first.detail::abi_configkeepsJSON_STRICT_NUL_HANDLINGreadable afterjson.hppundefines it at its end, in the ABI namespace, which already encodes the setting, so it always matches thebasic_jsonin use.out_of_range.416inexceptions.md, the C++20 module exports, an amalgamatedsingle_include/nlohmann/json_view.hpp(make amalgamate,check_amalgamation), a fuzzer, the benchmarks, Bazel, and theaspect: json_viewlabel inlabeler.yml.Performance
Measured on Apple M1 Max (Apple clang,
-O2) and an x86-64 KVM Haswell VPS pinned to one core (GCC 13 / Clang 18,-O2; the VPS is noisy, about ±5-10%). µs, best of 3 interleaved rounds of 15 runs; files from nativejson-benchmark.json_document::parsevsjson::parse(both at this PR):The core library is unaffected: this PR only adds code under
include/nlohmann/detail/view/and the new public headerjson_view.hpp.json.hpponly gains the internaldetail::abi_configconstant and one include.Tests
unit-json_view_builder.cpp: the node index compared withjson::parsefor handwritten, generated, and damaged documents under all option combinations; input fromstd::stringand from exact-size buffers with no read past the input under AddressSanitizer; deep nesting up to 100,000 levels; NUL, BOM, and whitespace cases; node integer bits and the array size limit; and the test-suite files.unit-json_view.cpp:materialize()equalsparse()for handwritten and generated documents, with duplicate keys, deep nesting, andJSON_DIAGNOSTICSparents; parse errors equalparse()'s, message included, for malformed inputs under all option combinations; every input kind, borrowed or owned; integer arguments androot()of a temporary document do not compile; the input size limit; reuse withread(), moves,shrink_to_fit(), and source offsets.unit-json_view_macros.cpp: includes the header withoutJSON_TEST_KEEP_MACROS, as users do, and checks that the view relies on no macrojson.hppundefines and leaks none of its ownNLOHMANN_VIEW_*macros.fuzzer-parse_json_view.cpp): accept, value, and exception parity withjson::parse, for astd::stringand an exact-size byte vector, with and without comments and trailing commas.Public API
No breaking changes. Everything is new and additive; nothing existing changes. The view API is new in this stack, so the following decisions are not breaking relative to a release, but they are visible to users:
<nlohmann/json_view.hpp>withbasic_json_document,basic_json_view, and the aliasesjson_document,json_view,ordered_json_document,ordered_json_view.json_viewhas nooperator bool: write!v.is_discarded().root()of an rvalue document is deleted (it would dangle); use a named document.parse,parse_copy,accept, andreadwith an integer second argument do not compile (deleted overloads).std::stringis copied, not moved.out_of_range.416(input of 4294967280 bytes or more).basic_json_document<BasicJsonType>requires 64-bit integer types (static_assert).json.hppgains the internaldetail::abi_configconstant (not part of the public API).Written by Claude Code.
🤖 Generated with Claude Code